/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using System.Threading;
using System.Diagnostics;
using System.IO;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject.Engine
{
	public class ExternalProcessBuildStep<TEngine> : BuildStep<TEngine>
		where TEngine : IBuildEngine
	{
		#region Public Properties
		public override string Name
		{
			get
			{
				return "Execute " + Path.GetFileName(file);
			}
		}

		public ICollection<string> Arguments
		{
			get
			{
				return new ReadOnlyCollection<string>(arguments);
			}
		}

		public string FilePath
		{
			get
			{
				return file;
			}
		}

		public string WorkingDirectory
		{
			get
			{
				return workingDirectory;
			}
			set
			{
				workingDirectory = value;
			}
		}
		#endregion

		#region Private / Protected
		protected EventWaitHandle CancelExecutionWaitHandler
		{
			get
			{
				return cancelExecutionHandle;
			}
		}

		private readonly string[] arguments;
		private readonly string file, argumentsParameter;
		private string workingDirectory;
		private readonly EventWaitHandle cancelExecutionHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ExternalProcessBuildStep" /> class.
		/// </summary>
		public ExternalProcessBuildStep(TEngine engine, string file, params string[] arguments)
			: this(engine, false, file, arguments)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ExternalProcessBuildStep" /> class.
		/// </summary>
		public ExternalProcessBuildStep(TEngine engine, bool continueOnError, string file, params string[] arguments)
			: base(engine)
		{
			if (string.IsNullOrEmpty(file))
				throw new ArgumentException(Errors.NullOrEmpty, "file");

			this.file = file;
			this.arguments = (arguments == null) ? new string[0] : arguments;

			if (this.arguments.Length > 0)
				argumentsParameter = string.Join(" ", this.arguments);

			ContinueOnError = continueOnError;
		}
		#endregion

		#region Methods
		protected sealed override void ExecuteInternal(BuildContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context", Errors.OutOfContext);

			string prevDir = null;

			if (!string.IsNullOrEmpty(workingDirectory))
			{
				prevDir = context.CurrentDirectory;
				context.CurrentDirectory = workingDirectory;
			}

			try
			{
				context.Invoke(delegate()
				{
					context.TraceLine();
					context.TraceLine("{0} {1}", file, string.Join(" ", arguments));
					context.TraceLine();
				});

				cancelExecutionHandle.Reset();

				int code = ExecuteExternalProcess(context);

				if (!context.Canceled && code != 0)
				{
					context.TraceLine("Non-zero exit code: " + code);

					throw new ExternalProcessException(file, code);
				}
			}
			finally
			{
				if (prevDir != null)
					context.CurrentDirectory = prevDir;
			}
		}

		protected sealed override void CancelInternal(BuildContext context)
		{
			cancelExecutionHandle.Set();
		}

		[System.Diagnostics.DebuggerHidden]
		protected virtual int ExecuteExternalProcess(BuildContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context", Errors.OutOfContext);

			using (Process process = new Process())
			{
				if (argumentsParameter != null)
					process.StartInfo.Arguments = argumentsParameter;

				process.StartInfo.FileName = file;
				process.StartInfo.CreateNoWindow = true;
				process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
				process.StartInfo.RedirectStandardOutput = true;
				process.StartInfo.RedirectStandardError = true;
				process.StartInfo.WorkingDirectory = context.CurrentDirectory;
				process.StartInfo.UseShellExecute = false;

				if (context.Canceled)
					return 0;

				process.Start();

				while (!process.StandardOutput.EndOfStream && !cancelExecutionHandle.WaitOne(0, false))
				{
					string line = process.StandardOutput.ReadLine();

					if (!string.IsNullOrEmpty(line))
						context.TraceLine(line);
				}

				if (!context.Canceled)
				{
					while (!process.StandardError.EndOfStream && !cancelExecutionHandle.WaitOne(0, false))
					{
						string line = process.StandardError.ReadLine();

						if (!string.IsNullOrEmpty(line))
							context.TraceLine(line);
					}
				}

				if (context.Canceled)
				{
					try
					{
						if (!process.CloseMainWindow())
							process.Kill();
					}
					// This exception occurred in testing for the Kill method if the process 
					// already exited, such as when it's canceled by the user but the process 
					// has a chance to finish first before this code block is entered.
					catch (InvalidOperationException) { }
					// This execption occurred in testing for Kill with the message, "Access denied".
					// The program appeared to be closed regardless (MRefBuilder.exe).
					// The MSDN docs say something about the process terminating during the
					// call, which I assume is the most likely cause.
					catch (System.ComponentModel.Win32Exception) { }
				}
				else
					process.WaitForExit();

				return (context.Canceled) ? 0 : process.ExitCode;
			}
		}

		protected override void Dispose(bool disposing)
		{
			try
			{
				if (disposing)
					cancelExecutionHandle.Close();
			}
			finally
			{
				base.Dispose(disposing);
			}
		}

		public override string ToString()
		{
			StringBuilder text = new StringBuilder();

			text.AppendFormat("\"{0}\"", file);

			if (argumentsParameter != null)
				text.Append(" " + argumentsParameter);

			return text.ToString();
		}
		#endregion
	}
}
